ド定番OSS!AFNetworking 2.xの使い方
今更なんだよ?って気がしますが、うちのブログにAFNetworkingについての記事が無いので軽く書いてみます。
2.x系になって変わったこと
まず、一番の変更点はAFHTTPClientがいなくなったことでしょうか。変わりにAFHTTPOperationManagerやAFHTTPSessionManagerなるものや、AFXxxRequestSerializer、AFXxxResponseSerializerなどが追加になりました。また、動作可能なiOSのバージョンは6.0以降になってました。
なんだこれ?ってわけで早速触ってみます。
AFXxxManager
AFHTTPOperationManagerとAFHTTPSessionManagerがありますが、どうやらiOS 6.xに対応するのであればAFHTTPOperationManagerを、iOS 7.x以降であればAFHTTPSessionManagerを使うといいらしいです。もう既にお分かりかと思いますが、AFHTTPSessionManagerはiOS 7で追加されたNSURLSessionを使っているのでそれもそのはずです。
なので案件によって使い分ける必要です。受託開発の場合はまだiOS 6.xは切れなそうなので、AFHTTPOperationManagerを使うことが多くなりそうです。
2.x系からはこのAFXxxManagerを使用して通信処理を記述します。例えば、単にJSON データを取得するのであれば以下のように書きます。
AFHTTPOperationManagerの場合
// AFHTTPRequestOperationManagerを利用して、http://localhost/test.jsonからJSONデータを取得する AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [manager GET:@"http://localhost/test.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // 通信に成功した場合の処理 NSLog(@"responseObject: %@", responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // エラーの場合はエラーの内容をコンソールに出力する NSLog(@"Error: %@", error); }];
なんかAFHTTPClientのときとあんまり変わらない気もしますが、よりシンプルになった気がします。
GET〜のようにHTTPメソッドごとにPOST〜、PUT〜が定義されてます。あと、これらのメソッドは実行と同時に通信処理が開始されるので、戻り値のインスタンスで- startメソッドとか呼ぶ必要はありません。ここではコンビニエンスコンストラクタを使ってAFHTTPOperationManagerのインスタンスを生成しましたが、- initWithBaseURL:でベースURLを指定することもできます。その場合、これらのHTTPメソッドごとのメソッド内ではURLStringにはベースURL以下のパスを記述すれば良さげです。
ただし、- HTTPRequestOperationWithRequest:success:failure:の場合は自分でNSURLRequestインスタンスを生成するのでベースURLは関係ないようです。また、このメソッド使う場合はHTTPメソッド系のメソッド(ややこしい・・)のようにメソッド実行時に通信処理は開始されないので、明示的に- startメソッドを呼ばなければなりません。これはAFHTTPSessionManagerも一緒のようです。
AFHTTPSessionManagerの場合
ちなみに、AFHTTPSessionManagerではこう書きます。
// AFHTTPSessionManagerを利用して、http://localhost/test.jsonからJSONデータを取得する AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; [manager GET:@"http://localhost/test.json" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) { // 通信に成功した場合の処理 NSLog(@"responseObject: %@", responseObject); } failure:^(NSURLSessionDataTask *task, NSError *error) { // エラーの場合はエラーの内容をコンソールに出力する NSLog(@"Error: %@", error); }];
AFHTTPOperationManagerとの違いは、戻り値とBlocksの第1引数がAFHTTPRequestOperationではなく、NSURLSessionDataTaskのインスタンスになるんですね。
AFXxxRequestSerializerとAFXxxResponseSerializer
個人的に、この変更が一番キターーーーーってなりました。受託開発の場合、リクエストやレスポンスをごにょごにょすることが非常に多いので、これらを別クラスとして再設計してもらえるのは非常にありがたいです。 このAFXxxRequestSerializerとAFXxxResponseSerializerは文字通りリクエストとレスポンスをシリアライズしてくれるクラスです。当然、それぞれ個別に指定できます。例えばリクエストパラメータはJSONで、レスポンスはplistでってことも簡単です。
今までAFHTTPClientのサブクラスを作成してAPIの仕様に合わせた実装を書いていた場合、この辺が代用できそうです。
AFXxxRequestSerializer
AFXxxRequestSerializerは、NSMutableURLRequestを生成するためのクラスです。リクエストパラメータの形式ごとに以下の3つが用意されています。
- AFHTTPRequestSerializer
- HTTPリクエストパラメータ(デフォルト)
- AFJSONRequestSerializer
- JSON形式のリクエストパラメータ
- AFPropertyListRequestSerializer
- plist形式のリクエストパラメータ
どのAFXxxRequestSerializerを使用するかを指定するには以下のように書きます。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; // JSON形式のリクエストパラメータを送信する manager.requestSerializer = [AFJSONRequestSerializer serializer];
具体的には、AFHTTPRequestSerializerは、例えばGETメソッドであれば、http://localhost/?hoge=fugaのようにURLストリングにパラメータを設定し、POSTであればHTTPリクエストのボディにパラメータをセットします。
AFJSONRequestSerializerの場合は、HTTPリクエストのボディにJSON形式にシリアライズしたデータをセットします。同様に、AFPropertyListRequestSerializerの場合はHTTPリクエストのボディにplist形式にシリアライズしたデータをセットします。
このとき、送信するパラメータの形式に合わせて、HTTPリクエストヘッダにcontent-typeの設定も行ってくれます。
HTTPリクエストヘッダ
AFXxxRequestSerializerではリクエストヘッダに独自のパラメータを設定することもできます。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; // HTTPリクエストヘッダにアプリバージョンをセットする [manager.requestSerializer setValue:@"1.0.0" forHTTPHeaderField:@"app-version"];
タイムアウトとキャッシュポリー
タイムアウトやキャシュポリーはそれぞれNSURLRequestのtimeoutIntervalプロパティ、cachePolicyプロパティに設定します。この場合、AFXxxRequestSerializerのサブクラスを作成し、NSURLRequestのインスタンスを生成する唯一のメソッドである- requestWithMethod:URLString:parameters:をオーバーライドするのが手っ取り早そうです。
@implementation MyHTTPRequestSerializer - (NSMutableURLRequest *)requestWithMethod:(NSString *)method URLString:(NSString *)URLString parameters:(NSDictionary *)parameters { NSMutableURLRequest *request = [super requestWithMethod:method URLString:URLString parameters:parameters]; // タイムアウトとキャッシュポリシーを設定する request.timeoutInterval = kTimeoutInterval; request.cachePolicy = NSURLRequestReloadIgnoringCacheData; return request; } @end
AFXxxResponseSerializer
AFXxxResponseSerializerでは
- AFHTTPResponseSerializer
- HTTPレスポンス
- AFJSONResponseSerializer
- JSON形式のレスポンス(デフォルト)
- AFXMLParserResponseSerializer
- XML形式のレスポンス
- AFPropertyListResponseSerializer
- plist形式のレスポンス
- AFImageResponseSerializer
- 画像
- AFCompoundResponseSerializer
- 上記のResponseSerializerの複合
が用意されています。どのAFXxxResponseSerializerを使用するかを指定するには以下のように書きます。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; // plist形式のリクエストパラメータを送信する manager.responseSerializer = [AFPropertyListResponseSerializer serializer];
AFXMLDocumentResponseSerializerは残念ながらOS Xのみです。 AFXxxResponseSerializerはレスポンスのシリアライズだけでなく、content-typeやステータスコードによるバリデーションも行っています。例えば、AFXxxManagerのresponseSerializerプロパティがAFJSONResponseSerializerなのに、実際のレスポンスのcontent-typeがtext/htmlだったりするとエラー扱いとなります。それぞれ対応するcontent-typeは以下の通りです。
- AFHTTPResponseSerializer
- 指定なし
- AFJSONResponseSerializer
- application/json、text/json、text/javascript
- AFXMLParserResponseSerializer
- application/xml、text/xml
- AFPropertyListResponseSerializer
- application/x-plist
- AFImageResponseSerializer
- image/*(jpeg、png、gifなど)
AFXxxManagerのresponseSerializerプロパティにはデフォルトでAFJSONResponseSerializerがセットされており、確認用に適当なURLを読み込んでみようとすると「レスポンスがJSONじゃないよー」って言われちゃうので、注意してください。
AFCompoundResponseSerializerは少し特殊で、複数のAFXxxResponseSerializerをまとめて設定することが可能です。この場合、AFCompoundResponseSerializerのコンビニエンスコンストラクタである+ compoundSerializerWithResponseSerializers:の引数に、使用するAFXxxResponseSerializerインスタンスの配列を渡す必要があります。
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; // AFJSONResponseSerializer、AFHTTPResponseSerializerの順にレスポンスを解析 NSArray *responseSerializers = @[ [AFJSONResponseSerializer serializer], [AFHTTPResponseSerializer serializer] ]; AFCompoundResponseSerializer *responseSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:responseSerializers]; manager.responseSerializer = responseSerializer; [manager GET:@"http://localhost/test.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // 通信に成功した場合の処理 NSLog(@"responseObject: %@", [responseObject class]); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // エラーの場合はエラーの内容をコンソールに出力する NSLog(@"Error: %@", error); }];
どのAFXxxResponseSerializerが適用されるかは、配列を順に走査して適用できるAFXxxResponseSerializerがあればシリアライズを実行して終了します。ですので指定する順序に注意が必要です。例えば、
NSArray *responseSerializers = @[ [AFHTTPResponseSerializer serializer], [AFJSONResponseSerializer serializer] ];
とすると、JSON形式のレスポンスでもAFHTTPResponseSerializerがシリアライズを実行してしまい、responsObjectはNSDataとなってしまいます。
独自のリザルトコードに対応する
独自のリザルトコードに対応する場合は、AFXxxResponseSerializerのサブクラスを作成すると綺麗に書けるかもしれません。
@implementation MyJSONResponseSerializer - (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error { id responseObject = [super responseObjectForResponse:response data:data error:error]; if (error && *error) { return nil; } NSDictionary *json = (NSDictionary *)responseObject; NSNumber *myResultCode = json[@"resultCode"]; if (myResultCode.integerValue == 0) { // 独自API成功 return responseObject; } else { // 独自APIエラー NSString *errorMessage = json[@"errorMessage"]; NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"My API is error!: %@ (%d)", @"MyProject", nil), errorMessage, myResultCode], NSURLErrorFailingURLErrorKey: [response URL], AFNetworkingOperationFailingURLResponseErrorKey: response }; if (error) { *error = [[NSError alloc] initWithDomain:MyProjectErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo]; } return nil; } } @end
ファイルのダウンロード・アップロード(AFHTTPOperationManagerの場合)
AFHTTPOperationManagerを使用する場合、ファイルのダウンロード・アップロードの処理は以下のように記述します。AFHTTPOperationManagerの方はたぶんAFNetworking 1.3系とそこまで変わらない気がしてます。
ファイルのダウンロード
>// 「http://localhost/test.zip」をダウンロードする AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; // ダウンロード先のURLを設定したNSURLRequestインスタンスを生成する NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:@"GET" URLString:@"http://localhost/test.zip" parameters:nil]; // ダウンロード処理を実行するためのAFHTTPRequestOperationインスタンスを生成する AFHTTPRequestOperation *operation = [manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) { // ダウンロードに成功したらコンソールに成功した旨を表示する NSLog(@"ダウンロード完了!"); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // エラーの場合はエラーの内容をコンソールに出力する NSLog(@"Error: %@", error); }]; // データを受信する度に実行される処理を設定する [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { // ダウンロード中の進捗状況をコンソールに表示する NSLog(@"bytesRead: %@, totalBytesRead: %@, totalBytesExpectedToRead: %@, progress: %@", @(bytesRead), @(totalBytesRead), @(totalBytesRead), @((float)totalBytesRead / totalBytesExpectedToRead)); }]; // <Application_Home>/Documentsディレクトリのパスを取得する NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = paths[0]; // <Application_Home>/Documents/test.zip NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"test.zip"]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:filePath]) { [fileManager removeItemAtPath:filePath error:nil]; } // ファイルの保存先を「<Application_Home>/Documents/test.zip」に指定する operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:NO]; // ダウンロードを開始する [manager.operationQueue addOperation:operation];
ファイルのアップロード
// <Application_Home>/Documentsディレクトリのパスを取得する NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = paths[0]; // <Application_Home>/Documents/test.png NSString *imageFilePath = [documentDirectory stringByAppendingPathComponent:@"test.jpg"]; // 画像ファイルからNSDataインスタンスを生成する NSData *imageData = [NSData dataWithContentsOfFile:imageFilePath]; NSLog(@"imageData : %@", imageFilePath); // 「http://localhost/uploadfile.php」に<Application_Home>/Documents/test.pngをアップロードする AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer new]; // アップロード先のURLを設定したNSURLRequestインスタンスを生成する NSMutableURLRequest *request = [manager.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:@"http://localhost/uploadfile.php" parameters:@{ @"param1": @"その他のパラメータ1", @"param2": @"その他のパラメータ2" } constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { [formData appendPartWithFileData:imageData name:@"image1" fileName:@"test.jpg" mimeType:@"image/jpeg"]; } error:NULL]; // アップロード処理を実行するためのAFHTTPRequestOperationインスタンスを生成する AFHTTPRequestOperation *operation = [manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) { // アップロードに成S功したらコンソールに成功した旨を表示する // NSData型のresponseObjectをNSStringに変換する NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; // 取得したレスポンスデータをコンソールに出力する NSLog(@"responseStr: %@", responseStr); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // エラーの場合はエラーの内容をコンソールに出力する NSLog(@"Error: %@", error); }]; // データを送信する度に実行される処理を設定する [operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) { // アップロード中の進捗状況をコンソールに表示する NSLog(@"bytesWritten: %@, totalBytesWritten: %@, totalBytesExpectedToWrite: %@, progress: %@", @(bytesWritten), @(totalBytesWritten), @(totalBytesExpectedToWrite), @((float)totalBytesWritten / totalBytesExpectedToWrite)); }]; // アップロードを開始する [manager.operationQueue addOperation:operation];
AFHTTPSessionManagerを使用する場合
AFHTTPSessionManagerの場合は、AFHTTPOperationManagerの時と比べ少し扱いが異なます。というのも、AFHTTPSessionManagerではiOS 7から追加されたNSURLSessionDownloadTaskやらNSURLSessionUploadTaskやらを使います(ここに関しては後で詳しく書こう思います)。
リトライ処理
AFNetworkingでは、標準では自動リトライ処理に対応していません。なので、自前で実装する必要があります。自動リトライ処理を実装する方法としては、AFXxxManangerのサブクラスやカテゴリーを実装する方法があります。特にカテゴリーで実装する方法はshaioz/AFNetworking-AutoRetryに非常に良いサンプルがあるので、必要な方は参考にしてみるといいと思います。
まとめ
ざっくりとはこんな感じでしょうか。あとは、AFNetworkActivityLoggerを使ってログを出力したり、セキュリティポリシーの設定をいじってオレオレ証明書を使ってSSL通信通信したり(この辺の内容はiOS - AFNetworking 2.0 のまとめ - Qiita [キータ] にわかりやすく書いてありました!)なども簡単に行えるようになったようです。
1.3系と比べ、かなり使いやすくなった印象ですね。ほんと素晴らしいライブラリです!